/*
 * Copyright (c) 2008, Petr Panteleyev
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.panteleyev.html.syntax;

import java.awt.Color;
import java.awt.Font;
import java.util.ArrayList;

import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

/**
 *
 * @author Petr Panteleyev
 */
public class Highlighter implements Runnable {

    private ArrayList<Token> tokens = new ArrayList<Token>();
    private JTextPane textPane;

    private String text;
    private int curPos;

    public Highlighter(JTextPane textPane) {
        this.textPane = textPane;

        // set styles
        Style style = textPane.addStyle(TokenType.ENTITY.name(), null);
        StyleConstants.setForeground(style, Color.RED);

        style = textPane.addStyle(TokenType.TAG.name(), null);
        StyleConstants.setForeground(style, Color.BLUE);

        style = textPane.addStyle("default", null);
        StyleConstants.setForeground(style, Color.BLACK);
    }

    private void updateStyle(Style style, Font font) {
        StyleConstants.setFontFamily(style, font.getFamily());
        StyleConstants.setFontSize(style, font.getSize());
        StyleConstants.setBold(style, font.isBold());
        StyleConstants.setItalic(style, font.isItalic());
    }
    
    public void fontUpdated() {
        Font font = textPane.getFont();
        
        updateStyle(textPane.getStyle(TokenType.TAG.name()), font);
        updateStyle(textPane.getStyle(TokenType.ENTITY.name()), font);
        updateStyle(textPane.getStyle("default"), font);

        SwingUtilities.invokeLater(this);
    }

    @Override
    public void run() {
        highlight();
    }

    private void highlight() {
        StyledDocument doc = textPane.getStyledDocument();
        try {
            text = doc.getText(0, doc.getLength());
        } catch (BadLocationException ex) {
        }

        if (text == null) {
            return;
        }

        curPos = 0;
        while (curPos < text.length()) {
            switch (text.charAt(curPos)) {
                case '<' :
                    parseTag();
                    break;

                case '&' :
                    parseEntity();
                    break;

                default:
                    ++curPos;
                    break;
            }
        }

        UndoableEditListener[] listeners = ((AbstractDocument)doc).getUndoableEditListeners();
        for (UndoableEditListener l : listeners) {
            ((AbstractDocument)doc).removeUndoableEditListener(l);
        }
        
        doc.setCharacterAttributes(0, text.length(), textPane.getStyle("default"), true);

        for (Token token : tokens) {
            doc.setCharacterAttributes(
                token.getStartIndex(),
                token.getEndIndex() - token.getStartIndex() + 1,
                textPane.getStyle(token.getType().name()), true);
        }

        for (UndoableEditListener l : listeners) {
            ((AbstractDocument)doc).addUndoableEditListener(l);
        }

        tokens.clear();
    }

    @SuppressWarnings("empty-statement")
    private void parseTag() {
        if (++curPos == text.length()) {
            return;
        }

        int closingPosition = text.indexOf('>', curPos);
        if (closingPosition == -1) {
            return;
        }

        if (text.charAt(curPos) == '/') {
            ++curPos;
        }

        int index = curPos;
        char ch = 0;

        if ((index >= closingPosition) || !Character.isLetter(text.charAt(index))) {
            return;
        } else {
            while ((++index <= closingPosition) && Character.isLetter(ch = text.charAt(index)));

            Token token = (curPos < index)? new Token(TokenType.TAG, curPos, index - 1) : null;

            if (index <= closingPosition) {
                // skip space if any
                while ((ch == ' ') && (index <= closingPosition)) {
                    ch = text.charAt(++index);
                }

                if (ch == '>') {
                    tokens.add(token);
                    curPos = index + 1;
                } else {
                    if (ch == '/') {
                        tokens.add(token);
                        curPos = index + 2;  // crap
                    } else {
                        if (Character.isLetter(ch)) {
                            curPos = index;
                            if (parseAttributes()) {
                                tokens.add(token);
                            }
                        } else {
                            curPos = index;
                        }
                    }
                }
            } else {
                curPos = index;
            }
        }
    }

    private boolean parseAttributes() {
        return true;
    }

    private boolean parseDecEntity() {
        while ((curPos < text.length()) && Character.isDigit(text.charAt(curPos))) {
            ++curPos;
        }

        return (curPos < text.length()) && (text.charAt(curPos) == ';');
    }
    
    private boolean parseHexEntity() {
        while ((curPos < text.length()) && isHexDigit(text.charAt(curPos))) {
            ++curPos;
        }

        return (curPos < text.length()) && (text.charAt(curPos) == ';');
    }

    private boolean parseNumberEntity() {
        if (++curPos < text.length()) {
            char ch = text.charAt(curPos);
            if ((ch == 'x') || (ch == 'X')) {
                ++curPos;
                return parseHexEntity();
            } else {
                return parseDecEntity();
            }
        } else {
            return false;
        }
    }
    
    private boolean parseLetterEntity() {
        while ((curPos < text.length()) && Character.isLetterOrDigit(text.charAt(curPos))) {
            ++curPos;
        }
        
        return (curPos < text.length()) && (text.charAt(curPos) == ';');
    }

    private void parseEntity() {
        int startPos = curPos;

        // check for letter or number
        if (++curPos < text.length()) {
            char ch = text.charAt(curPos);

            boolean res = false;

            if (ch == '#') {
                res = parseNumberEntity();
            } else {
                if (Character.isLetter(ch)) {
                    res = parseLetterEntity();
                } else {
                    ++curPos;
                }
            }

            if (res) {
                tokens.add(new Token(TokenType.ENTITY, startPos, curPos++));
            }
        }
    }

    private static boolean isHexDigit(char ch) {
        return Character.isDigit(ch) || ((ch >= 'a') && (ch <= 'f')) || ((ch >= 'A') && (ch <= 'F'));                
    }

}
